﻿using System.Diagnostics.Contracts;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace LinqToOwin
{
  // TODO: Consider making this class instantiable and somehow publicly configurable;
  // E.g., implement the IRespondAsync interface and expose it on the host object as an assignable property; default to this implementation, with assignable public properties.
  internal static class Responder
  {
    private const string defaultStreamContentType = "image/png";
    private const string defaultBinaryContentType = "image/png";
    private const string defaultStringContentType = "text/html";
    private const string xmlContentType = "text/xml";
    private const bool inferXmlContentType = true;
    private const bool inferBinaryContentType = true;
    private const bool inferStreamContentType = true;

    public static Task RespondAsync(LinqToOwinContext context, object value)
    {
      Contract.Requires(context != null);
      Contract.Ensures(Contract.Result<Task>() != null);

      Stream stream;
      byte[] binary;
      var responder = value as IRespondAsync;

      if (responder != null)
      {
        return responder.RespondAsync(context);
      }
      else if (value is HttpStatusCode)
      {
        context.Response.StatusCode = (int)(HttpStatusCode)value;
      }
      else if ((stream = value as Stream) != null)
      {
        var target = context.Response.Body;

        if (stream != target)
        {
          if (inferStreamContentType)
          {
            var contentType = context.Response.ContentType;
            var missingContentType = string.IsNullOrWhiteSpace(contentType);

            if (missingContentType)
            {
              context.Response.ContentType = defaultStreamContentType;
            }
          }

          return stream.CopyToAsync(target);
        }
      }
      else if ((binary = value as byte[]) != null)
      {
        if (inferBinaryContentType)
        {
          var contentType = context.Response.ContentType;
          var missingContentType = string.IsNullOrWhiteSpace(contentType);

          if (missingContentType || contentType.ToUpperInvariant().Contains("HTML"))
          {
            context.Response.ContentType = defaultBinaryContentType;
          }
        }

        return context.Response.WriteAsync(binary);
      }
      else if (value != null && !(value is LinqToOwinContext))
      {
        var contentType = context.Response.ContentType;
        var missingContentType = string.IsNullOrWhiteSpace(contentType);

        /* We must be flexible enough so that multiple queries can piece together an XML document without some of the queries that aren't 
         * necessarily detected as XML reverting the content-type back to HTML.  Therefore, only the initial response is used to set the 
         * content-type header for XML.
         */
        var isXml = context.FirstResponseIsXml ?? (context.FirstResponseIsXml = IsXml(value)).Value;

        if (isXml || missingContentType || contentType.ToUpperInvariant().Contains("TEXT"))
        {
          /* We must not overwrite an XML compatible content-type that's been assigned by the user, thus we only set the content-type if
           * it contains HTML, which is the default content-type; otherwise, the content-type remains unmodified.
           */
          if (isXml && (missingContentType || contentType.ToUpperInvariant().Contains("HTML")))
          {
            context.Response.ContentType = xmlContentType;
          }
          else if (missingContentType)
          {
            context.Response.ContentType = defaultStringContentType;
          }

          return context.Response.WriteAsync(value.ToString());
        }
      }

      return System.Threading.Tasks.Task.FromResult(true);
    }

    internal static bool IsXml(object value)
    {
      if (value is XContainer)
      {
        return true;
      }

      var response = value as string;

      if (!inferXmlContentType || response == null || response.Length == 0)
      {
        return false;
      }
      else
      {
        return IsXml(response);
      }
    }

    private static bool IsXml(string value)
    {
      Contract.Requires(value != null);

      var startTag = new char[5];

      for (int i = 0, j = 0; i < value.Length; i++)
      {
        var current = value[i];

        if (j == 0 && char.IsWhiteSpace(current))
        {
          continue;
        }
        else
        {
          bool valid;
          switch (j)
          {
            case 0:
              valid = current == '<';
              break;
            case 1: // ? or not-h
              valid = current == '?' || char.IsLetter(current);
              break;
            case 2: // x or not-t
            case 3: // m or not-m
            case 4: // l or not-l
              valid = char.IsLetter(current);
              break;
            default:
              return startTag[1] == '?'
                   ? startTag[2] == 'x' && startTag[3] == 'm' && startTag[4] == 'l' && (current == '?' || char.IsWhiteSpace(current))
                   : (startTag[1] != 'h' || startTag[2] != 't' || startTag[3] != 'm' || startTag[4] != 'l' || (current != '>' && !char.IsWhiteSpace(current)));
          }

          if (!valid)
          {
            break;
          }

          startTag[j++] = current;
        }
      }

      return false;
    }
  }
}